package org.sample.commons.bean;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.sample.commons.lang.RandomUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;

public class BeanUtils {

    private static final Logger log = LoggerFactory.getLogger(BeanUtils.class);

    private static Cache<Class<?>, Map<String, PropertyMethod>> beanInfoCache = Caffeine.newBuilder().maximumSize(40L).build();

    private static final List<String> ignores = new ArrayList<>();

    static {
        ignores.add("class");
        ignores.add("hibernateLazyInitializer");
        ignores.add("handler");
        ignores.add("fieldHandler");
    }

    public static <T> T copyBean(Object source, T target) {
        Map<String, PropertyMethod> targetBeanInfo = getBeanInfo(target.getClass());
        Map<String, PropertyMethod> sourceBeanInfo = getBeanInfo(source.getClass());

        for (Map.Entry<String, PropertyMethod> entry : targetBeanInfo.entrySet()) {
            String name = entry.getKey();
            PropertyMethod sourceMethod = sourceBeanInfo.get(name);
            if (sourceMethod != null) {
                Object value = getValue(source, sourceMethod.ReadMethod);
                setValue(target, entry.getValue().WriteMethod, value);
            }
        }
        return target;
    }

    public static <T> T copyBean(Object source, T target, String ignore) {
        Map<String, PropertyMethod> targetBeanInfo = getBeanInfo(target.getClass());
        Map<String, PropertyMethod> sourceBeanInfo = getBeanInfo(source.getClass());

        for (Map.Entry<String, PropertyMethod> entry : targetBeanInfo.entrySet()) {
            String name = entry.getKey();
            if (ignore.contains(name))
                continue;
            PropertyMethod sourceMethod = sourceBeanInfo.get(name);
            if (sourceMethod != null) {
                Object value = getValue(source, sourceMethod.ReadMethod);
                setValue(target, entry.getValue().WriteMethod, value);
            }
        }
        return target;
    }

    public static <T> T copyBeanNotNull(Object source, T target) {
        Map<String, PropertyMethod> targetBeanInfo = getBeanInfo(target.getClass());
        Map<String, PropertyMethod> sourceBeanInfo = getBeanInfo(source.getClass());

        for (Map.Entry<String, PropertyMethod> entry : targetBeanInfo.entrySet()) {
            String name = entry.getKey();
            PropertyMethod sourceMethod = sourceBeanInfo.get(name);
            if (sourceMethod != null) {
                Object value = getValue(source, sourceMethod.ReadMethod);
                if (value != null)
                    setValue(target, entry.getValue().WriteMethod, value);
            }
        }
        return target;
    }

    public static void cleanEmpty(Object target) {
        Map<String, PropertyMethod> beanInfo = getBeanInfo(target.getClass());
        for (Map.Entry<String, PropertyMethod> entry : beanInfo.entrySet()) {
            PropertyMethod propertyMethod = entry.getValue();
            Object value = getValue(target, propertyMethod.ReadMethod);
            if ("".equals(value))
                setValue(target, propertyMethod.WriteMethod, null);
        }
    }

    public static Map<String, Object> toMap(Object target) {
        Map<String, PropertyMethod> beanInfo = getBeanInfo(target.getClass());
        Map<String, Object> result = new HashMap<>(beanInfo.size());

        for (Map.Entry<String, PropertyMethod> entry : beanInfo.entrySet()) {
            Object value = getValue(target, entry.getValue().ReadMethod);
            result.put(entry.getKey(), value);
        }
        return result;
    }

    public static Map<String, Object> toMapNotNull(Object target) {
        Map<String, PropertyMethod> beanInfo = getBeanInfo(target.getClass());
        Map<String, Object> result = new HashMap<>(beanInfo.size());

        for (Map.Entry<String, PropertyMethod> entry : beanInfo.entrySet()) {
            Object value = getValue(target, entry.getValue().ReadMethod);
            if (value != null)
                result.put(entry.getKey(), value);
        }
        return result;
    }

    public static Object getValue(Object target, String name) {
        if (target == null)
            return null;
        return getValue(target, getReadMethod(target.getClass(), name));
    }

    public static Object getValue(Object obj, Method getter) {
        try {
            return getter.invoke(obj);
        } catch (Exception e) {
            log.error("获取对象值失败", e);
            return null;
        }
    }

    public static void setValue(Object target, String name, Object value) {
        if (target == null)
            return;
        setValue(target, getWriteMethod(target.getClass(), name), value);
    }

    public static void setValue(Object obj, Method setter, Object value) {
        if (setter != null)
            if (value == null || setter.getParameterTypes()[0].isAssignableFrom(value.getClass()))
                try {
                    setter.invoke(obj, value);
                } catch (Exception e) {
                    log.error("设置对象值失败", e);
                }
    }

    public static Method getReadMethod(Class<?> clazz, String propertyName) {
        return getBeanInfo(clazz).get(propertyName).ReadMethod;
    }

    public static Method getWriteMethod(Class<?> clazz, String propertyName) {
        return getBeanInfo(clazz).get(propertyName).WriteMethod;
    }

    public static Map<String, PropertyMethod> getPropertyMethod(Class<?> clazz) {
        return Collections.unmodifiableMap(getBeanInfo(clazz));
    }

    private static Map<String, PropertyMethod> getBeanInfo(Class<?> clazz) {
        return beanInfoCache.get(clazz, aClass -> {
            BeanInfo beanInfo;
            try {
                beanInfo = Introspector.getBeanInfo(aClass);
            } catch (IntrospectionException e) {
                throw new RuntimeException(e);
            }

            PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
            Map<String, PropertyMethod> result = new HashMap<>(pds.length);

            for (PropertyDescriptor pd : pds) {
                String name = pd.getName();
                if (!ignores.contains(name))
                    result.put(name, new PropertyMethod(pd.getReadMethod(), pd.getWriteMethod()));
            }

            Introspector.flushFromCaches(aClass);
            return result;
        });
    }

    public static <T> T newInstance(Class<T> target) {
        try {
            return target.newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 生成供测试使用的Bean对象
     * 对象内部的属性均为随机数，默认对象深度为2
     */
    public static <T> T genBean(Class<T> target) throws Exception {
        return genBean(target, 2);
    }

    /**
     * 生成供测试使用的Bean对象
     * 对象内部的属性均为随机数，默认对象深度为2
     *
     * @param target 目标类类型
     * @param depth  对象深度（若对象内存在其他对象，深度则控制对象内对象的嵌套层数）
     */
    public static <T> T genBean(Class<T> target, int depth) throws Exception {
        if (depth-- < 1)
            return null;
        T result = target.newInstance();

        Map<String, PropertyMethod> pds = getBeanInfo(target);
        for (Map.Entry<String, PropertyMethod> pd : pds.entrySet()) {
            PropertyMethod propertyMethod = pd.getValue();
            Class<?> clazz = propertyMethod.ReadMethod.getReturnType();
            if (clazz.isAssignableFrom(Class.class))
                continue;
            Object value = genBaseData(clazz, depth, target, pd.getKey());
            setValue(result, propertyMethod.WriteMethod, value);
        }
        return result;
    }

    private static <T> Object genBaseData(Class<T> clazz, int depth, Class rootClass, String name) throws Exception {
        ThreadLocalRandom random = ThreadLocalRandom.current();

        if (clazz.isAssignableFrom(Boolean.class) || clazz.isAssignableFrom(Boolean.TYPE))
            return random.nextBoolean();
        if (clazz.isAssignableFrom(Byte.class) || clazz.isAssignableFrom(Byte.TYPE))
            return (byte) random.nextInt();
        if (clazz.isAssignableFrom(Short.class) || clazz.isAssignableFrom(Short.TYPE))
            return (short) random.nextInt(0, 255);
        if (clazz.isAssignableFrom(Integer.class) || clazz.isAssignableFrom(Integer.TYPE))
            return random.nextInt(0, 65535);
        if (clazz.isAssignableFrom(Long.class) || clazz.isAssignableFrom(Long.TYPE))
            return random.nextLong(0, 16777215);
        if (clazz.isAssignableFrom(Float.class) || clazz.isAssignableFrom(Float.TYPE))
            return random.nextFloat();
        if (clazz.isAssignableFrom(Double.class) || clazz.isAssignableFrom(Double.TYPE))
            return random.nextDouble();
        if (clazz.isAssignableFrom(Character.class) || clazz.isAssignableFrom(Character.TYPE))
            return RandomUtils.nextString(1).charAt(0);
        if (clazz.isAssignableFrom(String.class))
            return RandomUtils.nextString(9);
        if (clazz.isAssignableFrom(Date.class))
            return new Date();
        if (clazz.isAssignableFrom(LocalDate.class))
            return LocalDate.now();
        if (clazz.isAssignableFrom(LocalTime.class))
            return LocalTime.now();
        if (clazz.isAssignableFrom(LocalDateTime.class))
            return LocalDateTime.now();
        if (clazz.isAssignableFrom(List.class)) {
            Class<?> x = getGenericType(rootClass, name);
            Object o = genBaseData(x, depth, null, null);

            List<Object> list = new ArrayList<>(2);
            list.add(o);
            list.add(o);
            return list;
        }
        if (clazz.isArray()) {
            Object array = Array.newInstance(clazz.getComponentType(), 2);
            Object o = genBaseData(clazz.getComponentType(), depth, null, null);
            Array.set(array, 0, o);
            Array.set(array, 1, o);
            return array;
        }
        if (clazz.isEnum()) {
            Method method = clazz.getMethod("values");
            Object[] values = (Object[]) method.invoke(null);
            return values[random.nextInt(values.length)];
        }
        return genBean(clazz, depth);
    }

    public static Class getGenericType(Class rootClass, String fieldName) {
        try {
            Field field = rootClass.getDeclaredField(fieldName);
            ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
            return (Class) parameterizedType.getActualTypeArguments()[0];
        } catch (NoSuchFieldException e) {
            throw new RuntimeException(e);
        }
    }

    public static class PropertyMethod {
        protected final Method ReadMethod;
        protected final Method WriteMethod;

        public PropertyMethod(Method readMethod, Method writeMethod) {
            this.ReadMethod = readMethod;
            this.WriteMethod = writeMethod;
        }

        public Method getReadMethod() {
            return ReadMethod;
        }

        public Method getWriteMethod() {
            return WriteMethod;
        }
    }
}